package dryven.request.controller.result;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import dryven.annotations.DefaultResultType;
import dryven.model.binding.proxy.Binding;
import dryven.request.controller.ActionException;
import dryven.request.controller.ActionListener;
import dryven.request.controller.ControllerDescription;
import dryven.request.controller.ControllerFactory;
import dryven.request.http.Request;
import dryven.request.http.Response;

public class ForwardResult implements ActionResult, OriginatingControllerListener {

	private ControllerDescription forward;
	private ControllerFactory factory;

	public ForwardResult(Binding<ActionResult> binding) {
		this(new ControllerDescription(binding));
	}
	
	public ForwardResult(ControllerDescription forward) {
		super();
		this.forward = forward;
	}
	
	public void injectDependencies(ControllerFactory factory) {
		this.factory = factory;
	}
	
	@Override
	public ActionResult process(Request req, Response resp) {
		Object controller = factory.instanciateController(forward, req);
		Method action = forward.getAction();
		Object[] params = forward.getParameters();
		
		ActionListener listener = null;
		ActionResult actionResult = null;
		
		if(ActionListener.class.isAssignableFrom(controller.getClass())) {
			listener = (ActionListener) controller;
			actionResult = listener.beforeAction(forward,req,resp);
			if(actionResult!=null) {
				return actionResult;
			}
		}
		
		try {
			Object result = action.invoke(controller, params);
			if(ActionResult.class.isAssignableFrom(action.getReturnType())) {
				actionResult = (ActionResult) result;
			} else {
				actionResult = wrapInActionResult(controller, action, result);
			}
		} catch(Throwable e) {
			if(e.getCause()!=null) {
				e = e.getCause();
			}
			if(listener!=null) {
				actionResult = listener.handleException(e,forward);
			}
			if(actionResult==null) {
				throw new RuntimeException(e);
			}
		}
		
		if(listener!=null) {
			listener.afterAction();
		}
		
		if(actionResult!=null && OriginatingControllerListener.class.isAssignableFrom(actionResult.getClass())) {
			OriginatingControllerListener l = (OriginatingControllerListener)actionResult;
			l.passOriginatingController(forward);
		}
		
		return actionResult;
	}

	protected ActionResult wrapInActionResult(Object controller, Method action,
			Object result) throws InstantiationException,
			IllegalAccessException, InvocationTargetException {
		ActionResult actionResult;
		Class<?> controllerType = controller.getClass();
		//look for annotations in parent types
		DefaultResultType resultTypeAnno = null;
		do {
			resultTypeAnno = controllerType.getAnnotation(DefaultResultType.class);
			controllerType = controllerType.getSuperclass();
		} while(resultTypeAnno==null && controllerType!=null);
		
		if(resultTypeAnno==null) {
			throw new RuntimeException("Return type of controller action "+controller.getClass().getName()+"."+action.getName()+" is not assignable to "+ActionResult.class.getSimpleName()+" and no @"+DefaultResultType.class.getSimpleName()+" annotation was found");
		}
		Class<? extends ActionResult> resultType = resultTypeAnno.value();
		Constructor<?> resultCtor = null;
		for (Constructor<?> ctor : resultType.getConstructors()) {
			if(ctor.getParameterTypes().length==1 && (result==null || ctor.getParameterTypes()[0].isAssignableFrom(result.getClass()))) {
				resultCtor = ctor;
				break;
			}
		}
		if(resultCtor==null) {
			throw new RuntimeException(String.format("Not suitable constructor found in %s type %s for parameter type %s", ActionResult.class.getSimpleName(),resultType.getName(),result==null?"null":result.getClass().getName()));
		}
		actionResult = (ActionResult) resultCtor.newInstance(result);
		return actionResult;
	}

	@Override
	public void passOriginatingController(ControllerDescription cd) {
		if(forward==null) {
			return;
		}
		for(int i=0;i<forward.getParameterCount();++i) {
			Object param = forward.getActionParameter(i);
			if(param!=null && OriginatingControllerListener.class.isAssignableFrom(param.getClass())) {
				((OriginatingControllerListener)param).passOriginatingController(cd);
			}
		}
	}

	
}
